<!DOCTYPE HTML>
<html lang="zh-CN">


<head>
    <meta charset="utf-8">
    <meta name="keywords" content="实现Raft协议：Part 1 - 选主, 码道诚公">
    <meta name="description" content="">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <meta name="renderer" content="webkit|ie-stand|ie-comp">
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="format-detection" content="telephone=no">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <meta name="baidu-site-verification" content="code-oqVlgh2wQV" />
    <meta name="google-site-verification" content="Z-8Fm46WY6NL_3kxilmoqToW4jnvyF7boTgciTLBXUs" />
    <!-- Global site tag (gtag.js) - Google Analytics -->


    <title>实现Raft协议：Part 1 - 选主 | 码道诚公</title>
    <link rel="icon" type="image/png" href="/favicon.png">

    <link rel="stylesheet" type="text/css" href="/libs/awesome/css/all.css">
    <link rel="stylesheet" type="text/css" href="/libs/materialize/materialize.min.css">
    <link rel="stylesheet" type="text/css" href="/libs/aos/aos.css">
    <link rel="stylesheet" type="text/css" href="/libs/animate/animate.min.css">
    <link rel="stylesheet" type="text/css" href="/libs/lightGallery/css/lightgallery.min.css">
    <link rel="stylesheet" type="text/css" href="/css/matery.css">
    <link rel="stylesheet" type="text/css" href="/css/my.css">

    <script src="/libs/jquery/jquery.min.js"></script>

<meta name="generator" content="Hexo 5.2.0">
<style>.github-emoji { position: relative; display: inline-block; width: 1.2em; min-height: 1.2em; overflow: hidden; vertical-align: top; color: transparent; }  .github-emoji > span { position: relative; z-index: 10; }  .github-emoji img, .github-emoji .fancybox { margin: 0 !important; padding: 0 !important; border: none !important; outline: none !important; text-decoration: none !important; user-select: none !important; cursor: auto !important; }  .github-emoji img { height: 1.2em !important; width: 1.2em !important; position: absolute !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; user-select: none !important; cursor: auto !important; } .github-emoji-fallback { color: inherit; } .github-emoji-fallback img { opacity: 0 !important; }</style>
<link rel="alternate" href="/atom.xml" title="码道诚公" type="application/atom+xml">
<link rel="stylesheet" href="/css/prism-tomorrow.css" type="text/css"></head>




<body>
    <header class="navbar-fixed">
    <nav id="headNav" class="bg-color nav-transparent">
        <div id="navContainer" class="nav-wrapper container">
            <div class="brand-logo">
                <a href="/" class="waves-effect waves-light">
                    
                    <img src="/medias/logo.png" class="logo-img" alt="LOGO">
                    
                    <span class="logo-span">码道诚公</span>
                </a>
            </div>
            

<a href="#" data-target="mobile-nav" class="sidenav-trigger button-collapse"><i class="fas fa-bars"></i></a>
<ul class="right nav-menu">
  
  <li class="hide-on-med-and-down nav-item">
    
    <a href="/" class="waves-effect waves-light">
      
      <i class="fas fa-home" style="zoom: 0.6;"></i>
      
      <span>首页</span>
    </a>
    
  </li>
  
  <li class="hide-on-med-and-down nav-item">
    
    <a href="/tags" class="waves-effect waves-light">
      
      <i class="fas fa-tags" style="zoom: 0.6;"></i>
      
      <span>标签</span>
    </a>
    
  </li>
  
  <li class="hide-on-med-and-down nav-item">
    
    <a href="/categories" class="waves-effect waves-light">
      
      <i class="fas fa-bookmark" style="zoom: 0.6;"></i>
      
      <span>分类</span>
    </a>
    
  </li>
  
  <li class="hide-on-med-and-down nav-item">
    
    <a href="/archives" class="waves-effect waves-light">
      
      <i class="fas fa-archive" style="zoom: 0.6;"></i>
      
      <span>归档</span>
    </a>
    
  </li>
  
  <li class="hide-on-med-and-down nav-item">
    
    <a href="/about" class="waves-effect waves-light">
      
      <i class="fas fa-user-circle" style="zoom: 0.6;"></i>
      
      <span>关于</span>
    </a>
    
  </li>
  
  <li class="hide-on-med-and-down nav-item">
    
    <a href="/contact" class="waves-effect waves-light">
      
      <i class="fas fa-comments" style="zoom: 0.6;"></i>
      
      <span>留言板</span>
    </a>
    
  </li>
  
  <li class="hide-on-med-and-down nav-item">
    
    <a href="/friends" class="waves-effect waves-light">
      
      <i class="fas fa-address-book" style="zoom: 0.6;"></i>
      
      <span>友情链接</span>
    </a>
    
  </li>
  
  <li>
    <a href="#searchModal" class="modal-trigger waves-effect waves-light">
      <i id="searchIcon" class="fas fa-search" title="搜索" style="zoom: 0.85;"></i>
    </a>
  </li>
</ul>


<div id="mobile-nav" class="side-nav sidenav">

    <div class="mobile-head bg-color">
        
        <img src="/medias/logo.png" class="logo-img circle responsive-img">
        
        <div class="logo-name">码道诚公</div>
        <div class="logo-desc">
            
            Never really desperate, only the lost of the soul.
            
        </div>
    </div>

    

    <ul class="menu-list mobile-menu-list">
        
        <li class="m-nav-item">
	  
		<a href="/" class="waves-effect waves-light">
			
			    <i class="fa-fw fas fa-home"></i>
			
			首页
		</a>
          
        </li>
        
        <li class="m-nav-item">
	  
		<a href="/tags" class="waves-effect waves-light">
			
			    <i class="fa-fw fas fa-tags"></i>
			
			标签
		</a>
          
        </li>
        
        <li class="m-nav-item">
	  
		<a href="/categories" class="waves-effect waves-light">
			
			    <i class="fa-fw fas fa-bookmark"></i>
			
			分类
		</a>
          
        </li>
        
        <li class="m-nav-item">
	  
		<a href="/archives" class="waves-effect waves-light">
			
			    <i class="fa-fw fas fa-archive"></i>
			
			归档
		</a>
          
        </li>
        
        <li class="m-nav-item">
	  
		<a href="/about" class="waves-effect waves-light">
			
			    <i class="fa-fw fas fa-user-circle"></i>
			
			关于
		</a>
          
        </li>
        
        <li class="m-nav-item">
	  
		<a href="/contact" class="waves-effect waves-light">
			
			    <i class="fa-fw fas fa-comments"></i>
			
			留言板
		</a>
          
        </li>
        
        <li class="m-nav-item">
	  
		<a href="/friends" class="waves-effect waves-light">
			
			    <i class="fa-fw fas fa-address-book"></i>
			
			友情链接
		</a>
          
        </li>
        
        
    </ul>
</div>


        </div>

        
    </nav>

</header>

    



<div class="bg-cover pd-header post-cover" style="background-image: url('/medias/featureimages/21.jpg')">
    <div class="container" style="right: 0px;left: 0px;">
        <div class="row">
            <div class="col s12 m12 l12">
                <div class="brand">
                    <h1 class="description center-align post-title">实现Raft协议：Part 1 - 选主</h1>
                </div>
            </div>
        </div>
    </div>
</div>




<main class="post-container content">

    
    <link rel="stylesheet" href="/libs/tocbot/tocbot.css">
<style>
    #articleContent h1::before,
    #articleContent h2::before,
    #articleContent h3::before,
    #articleContent h4::before,
    #articleContent h5::before,
    #articleContent h6::before {
        display: block;
        content: " ";
        height: 100px;
        margin-top: -100px;
        visibility: hidden;
    }

    #articleContent :focus {
        outline: none;
    }

    .toc-fixed {
        position: fixed;
        top: 64px;
    }

    .toc-widget {
        width: 345px;
        padding-left: 20px;
    }

    .toc-widget .toc-title {
        margin: 35px 0 15px 0;
        padding-left: 17px;
        font-size: 1.5rem;
        font-weight: bold;
        line-height: 1.5rem;
    }

    .toc-widget ol {
        padding: 0;
        list-style: none;
    }

    #toc-content {
        height: calc(100vh - 250px);
        overflow: auto;
    }

    #toc-content ol {
        padding-left: 10px;
    }

    #toc-content ol li {
        padding-left: 10px;
    }

    #toc-content .toc-link:hover {
        color: #42b983;
        font-weight: 700;
        text-decoration: underline;
    }

    #toc-content .toc-link::before {
        background-color: transparent;
        max-height: 25px;

        position: absolute;
        right: 23.5vw;
        display: block;
    }

    #toc-content .is-active-link {
        color: #42b983;
    }

    #floating-toc-btn {
        position: fixed;
        right: 15px;
        bottom: 76px;
        padding-top: 15px;
        margin-bottom: 0;
        z-index: 998;
    }

    #floating-toc-btn .btn-floating {
        width: 48px;
        height: 48px;
    }

    #floating-toc-btn .btn-floating i {
        line-height: 48px;
        font-size: 1.4rem;
    }
</style>
<div class="row">
    <div id="main-content" class="col s12 m12 l9">
        <!-- 文章内容详情 -->
<div id="artDetail">
    <div class="card">
        <div class="card-content article-info">
            <div class="row tag-cate">
                <div class="col s7">
                    
                    <div class="article-tag">
                        
                            <a href="/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7/">
                                <span class="chip bg-color">分布式一致性</span>
                            </a>
                        
                            <a href="/tags/Raft/">
                                <span class="chip bg-color">Raft</span>
                            </a>
                        
                            <a href="/tags/%E8%AF%91%E6%96%87/">
                                <span class="chip bg-color">译文</span>
                            </a>
                        
                    </div>
                    
                </div>
                <div class="col s5 right-align">
                    
                    <div class="post-cate">
                        <i class="fas fa-bookmark fa-fw icon-category"></i>
                        
                            <a href="/categories/%E5%88%86%E5%B8%83%E5%BC%8F/" class="post-category">
                                分布式
                            </a>
                        
                            <a href="/categories/%E5%88%86%E5%B8%83%E5%BC%8F/%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE/" class="post-category">
                                一致性协议
                            </a>
                        
                            <a href="/categories/%E5%88%86%E5%B8%83%E5%BC%8F/%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE/Raft/" class="post-category">
                                Raft
                            </a>
                        
                    </div>
                    
                </div>
            </div>

            <div class="post-info">
                
                <div class="post-date info-break-policy">
                    <i class="far fa-calendar-minus fa-fw"></i>发布日期:&nbsp;&nbsp;
                    2021-03-04
                </div>
                

                

                
                <div class="info-break-policy">
                    <i class="far fa-file-word fa-fw"></i>文章字数:&nbsp;&nbsp;
                    8.2k
                </div>
                

                
                <div class="info-break-policy">
                    <i class="far fa-clock fa-fw"></i>阅读时长:&nbsp;&nbsp;
                    30 分
                </div>
                

                
            </div>
        </div>
        <hr class="clearfix">
        <div class="card-content article-card-content">
            <div id="articleContent">
                <p>翻译自<a target="_blank" rel="noopener" href="https://eli.thegreenplace.net/archives/all">Eli Bendersky</a>的<a target="_blank" rel="noopener" href="https://eli.thegreenplace.net/2020/implementing-raft-part-1-elections/">系列博客</a>，已获得原作者授权。</p>
<p>本文是系列文章中的第一部分，本系列文章旨在介绍Raft分布式一致性协议及其Go语言实现。文章的完整列表如下：</p>
<ul>
<li>序言：<a href="/posts/772d24e8.html" title="介绍">介绍</a></li>
<li>第一部分：选主(本文)</li>
<li>第二部分：<a href="/posts/25bc8c4b.html" title="指令和日志复制">指令和日志复制</a></li>
<li>第三部分：<a href="/posts/9e1d4cec.html" title="持久性和优化">持久性和优化</a></li>
</ul>
<p>在这一部分，我会介绍我们的Raft实现代码的结构，并重点介绍算法的<em>选主</em>部分。本文的代码包括一个全功能的测试工具和一些您可以用来测试系统的案例。但是它不会响应客户端的请求，也不好维护日志，这些功能会在第2部分添加。</p>
<h3 id="代码结构"><a href="#代码结构" class="headerlink" title="代码结构"></a>代码结构</h3><p>简单介绍一下Raft实现的代码结构，本系列的所有部分都是通用的。</p>
<p>通常来说，Raft都被实现为一个可嵌入某些服务的对象。因为我们不会真正地开发一个服务，只是研究Raft协议本身，所以创建了一个简单的<code>Server</code>类型，其中包装了一个<code>ConsensusModule</code>类型，以期尽可能隔离出代码中最有趣的部分：</p>
<p><img src="/posts/be586517/server-cm-architecture.png" alt="Architecture of a consensus module embedded into a server"></p>
<p>一致性模块(CM)实现了Raft算法的核心，在<code>raft.go</code>文件中。该模块从网络细节以及与集群中其它副本的连接中完成抽象出来，<code>ConsensusModule</code>中与网络相关的唯一字段就是：</p>
<pre class=" language-go"><code class="language-go"><span class="token comment" spellcheck="true">// id 是一致性模块中的服务器ID</span>
id <span class="token builtin">int</span>

<span class="token comment" spellcheck="true">// peerIds 是集群中所有同伴的ID列表</span>
peerIds <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token builtin">int</span>

<span class="token comment" spellcheck="true">// server 是包含该CM的服务器. 该字段用于向其它同伴发起RPC调用</span>
server <span class="token operator">*</span>Server</code></pre>
<p>在实现过程中，每个Raft副本将集群中的其它副本称为“同伴”。集群中的每个同伴都有一个唯一的数值ID，以及记录其同伴ID的列表。<code>server</code>字段是指向模块所在<code>Server</code>*（在<code>server.go</code>中实现）的指针，后者可以允许<code>ConsensusModule</code>将消息发送给同伴。稍后我们将看到这是如何完成的。</p>
<p>这样设计的目的就是要将所有的网络细节排除在外，从而专注于Raft算法本身。总之，要将Raft论文对照到本实现的话。你只需要<code>ConsensusModule</code>类及其方法。<code>Server</code>代码是一个非常简单的Go语言网络框架，有一些细微的复杂之处来应对严格的测试。本系列文章中，我不会花时间讨论它。但是如果有什么不清楚的地方，请随意提问。</p>
<h3 id="Raft服务器状态"><a href="#Raft服务器状态" class="headerlink" title="Raft服务器状态"></a>Raft服务器状态</h3><p>总的来说，Raft CM就是一个具有3个状态的状态机<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>：</p>
<p><img src="/posts/be586517/raft-highlevel-state-machine.png" alt="Raft high level state machine (实现Raft协议：Part 1 - 选主.assets/raft-highlevel-state-machine.png)"></p>
<p>因为在序言中 花费了很多篇幅解释Raft如何帮助<em>实现</em>状态机，所以对这里可能会有一点困惑，但是必须说明一下，这里的术语<code>状态</code>含义是不同的。Raft是一个实现任意复制状态机的算法，但是Raft内部也包含一个小的状态机。后面的章节中，某个地方的<em>状态</em>是什么含义都可以结合上下弄清楚——如果不能的话，我肯定是指出来的。</p>
<p>在一个典型的稳态场景中，集群中有一个服务器是领导者，而其它副本都是追随者。尽管我们很希望系统可以一直这样运行下去，但是Raft协议的目标就是容错。因此，我们会花费大部分时间来讨论一下非典型的<em>故障场景</em>，如某些服务器崩溃，其它服务器断开连接，等等。</p>
<p>之前提到过，Raft使用的是<em>强领导</em>模型。领导者响应客户端请求，向日志中添加新条目并将其复制给其它追随者。每个追随者随时准备接管领导权，以防领导者故障或停止通信。这也就是上图中从<code>追随者</code>向<code>候选人(Candidate)</code>的转变（“等待超时，开始选举”）。</p>
<h3 id="任期（Terms）"><a href="#任期（Terms）" class="headerlink" title="任期（Terms）"></a>任期（Terms）</h3><p>就是正常的选举一样，Raft中也有<em>任期</em>。任期指的就是某一服务器作为领导者的一段时间。新的选举会触发新的任期，而且Raft算法保证在给定的任期中只有一个领导者。</p>
<p>但是这个比喻就到此为止吧，因为Raft中的选主跟真正的选举区别还是很大的。在Raft中，选举是更具协同性的，候选者的目标不是要不惜一切代价赢得选举——所有候选人有一个共同的目标，那就是在任意给定的任期都有<em>合适</em>的服务器赢得选举。什么稍后会详细讨论”合适“的含义。</p>
<h3 id="选举定时器"><a href="#选举定时器" class="headerlink" title="选举定时器"></a>选举定时器</h3><p>Raft算法中的一个关键组成部分就是<em>选举定时器</em>。这是每个追随者都会持续运行的定时器，每次接收到当前领导者的消息时就重新启动它。领导者会发送周期性的心跳，因此当追随者接收不到这些心跳信号时，他会认为当前领导者出现故障或者断开连接，并开始新一轮选举（切换为候选者状态）。</p>
<p><strong>问：</strong>所有的追随者不会同时变成候选人吗？</p>
<p><strong>答：</strong>选举定时器是随机的，这也是Raft协议保持简单性的关键之一。Raft通过这种随机化来降低多个追随者同时进行选举的可能性。但是即便它们在同一时刻变成候选人，在任何一个任期内只有一个服务器会被选为领导者。在极少数情况下，会出现投票分裂导致没有候选人获胜，此时将进行新一轮的选举（使用新的任期）。虽然在理论上有可能会永远在重新选举，但是每多一轮选举，发生这种情况的概率会大大降低。</p>
<p><strong>问：</strong>如果追随者从集群中断开连接（分区）怎么办？它不会因为没有收到领导者的消息而开始选举吗？</p>
<p><strong>答：</strong>这就是网络分区问题的隐蔽性，因为追随者无法区分谁被分割了。确实，这个追随者会开启新一轮选举。但是，如果这个追随者被断开连接，那么这次选举也会无果而终——因为它无法联系到其它同伴，也就不会获得任何选票。它可能会在候选者状态一直自旋（每隔一段时间就开启新一轮的选举）直到重新接入集群中。稍后我们会详细讨论这种情况。</p>
<h3 id="同伴间RPC"><a href="#同伴间RPC" class="headerlink" title="同伴间RPC"></a>同伴间RPC</h3><p>Raft协议中，同伴间会发送两类RPC请求。详细的参数和规则可以参考论文中的<code>Figure 2</code>，或者本文的<a href="#%E9%99%84">附录</a>。这里简单说明一下两种请求：</p>
<ul>
<li><code>RequestVote(RV)</code>：只有候选人状态下会使用。在一轮选举中，候选人通过该接口向同伴请求选票。返回值中包含是否同意投票的标志。</li>
<li><code>AppendEntries(AE)</code>：只有领导者状态下使用。领导者通过该RPC将日志条目复制给追随者，也用来发送心跳。即使没有要复制的日志条目，也会定期向追随者发送该RPC请求。</li>
</ul>
<p>明眼人可能看出来追随者不会发送任何的RPC请求。这是对的，追随者不会向同伴发起RPC请求，但是它们在后台会运行一个选举定时器。如果定时器结束之前没有接收到当前领导者的信息，追随者就变成候选人并开始发送RV请求。</p>
<h3 id="实现选举定时器"><a href="#实现选举定时器" class="headerlink" title="实现选举定时器"></a>实现选举定时器</h3><p>是时候开始研究代码了。除非特别说明，否则下面展示的所有代码示例都出自<a target="_blank" rel="noopener" href="https://github.com/eliben/raft/blob/master/part1/raft.go">这个文件</a>。我不会把<code>ConsensusModule</code>结构体的所有字段——你可以在代码文件中去查看。</p>
<p>我们的CM模块通过在goroutime中执行以下函数来实现选举定时器：</p>
<pre class=" language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>cm <span class="token operator">*</span>ConsensusModule<span class="token punctuation">)</span> <span class="token function">runElectionTimer</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    timeoutDuration <span class="token operator">:=</span> cm<span class="token punctuation">.</span><span class="token function">electionTimeout</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    termStarted <span class="token operator">:=</span> cm<span class="token punctuation">.</span>currentTerm
    cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"election timer started (%v), term=%d"</span><span class="token punctuation">,</span> timeoutDuration<span class="token punctuation">,</span> termStarted<span class="token punctuation">)</span>

    <span class="token comment" spellcheck="true">/*
      循环会在以下条件结束：
      1 - 发现不再需要选举定时器
      2 - 选举定时器超时，CM变为候选人
      对于追随者而言，定时器通常会在CM的整个生命周期中一直在后台运行。
    */</span>
    ticker <span class="token operator">:=</span> time<span class="token punctuation">.</span><span class="token function">NewTicker</span><span class="token punctuation">(</span><span class="token number">10</span> <span class="token operator">*</span> time<span class="token punctuation">.</span>Millisecond<span class="token punctuation">)</span>
    <span class="token keyword">defer</span> ticker<span class="token punctuation">.</span><span class="token function">Stop</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token keyword">for</span> <span class="token punctuation">{</span>
        <span class="token operator">&lt;-</span>ticker<span class="token punctuation">.</span>C

        cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token comment" spellcheck="true">// CM不再需要定时器</span>
        <span class="token keyword">if</span> cm<span class="token punctuation">.</span>state <span class="token operator">!=</span> Candidate <span class="token operator">&amp;&amp;</span> cm<span class="token punctuation">.</span>state <span class="token operator">!=</span> Follower <span class="token punctuation">{</span>
            cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"in election timer state=%s, bailing out"</span><span class="token punctuation">,</span> cm<span class="token punctuation">.</span>state<span class="token punctuation">)</span>
            cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
            <span class="token keyword">return</span>
        <span class="token punctuation">}</span>

        <span class="token comment" spellcheck="true">// 任期变化</span>
        <span class="token keyword">if</span> termStarted <span class="token operator">!=</span> cm<span class="token punctuation">.</span>currentTerm <span class="token punctuation">{</span>
            cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"in election timer term changed from %d to %d, bailing out"</span><span class="token punctuation">,</span> termStarted<span class="token punctuation">,</span> cm<span class="token punctuation">.</span>currentTerm<span class="token punctuation">)</span>
            cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
            <span class="token keyword">return</span>
        <span class="token punctuation">}</span>

        <span class="token comment" spellcheck="true">// 如果在超时之前没有收到领导者的信息或者给其它候选人投票，就开始新一轮选举</span>
        <span class="token keyword">if</span> elapsed <span class="token operator">:=</span> time<span class="token punctuation">.</span><span class="token function">Since</span><span class="token punctuation">(</span>cm<span class="token punctuation">.</span>electionResetEvent<span class="token punctuation">)</span><span class="token punctuation">;</span> elapsed <span class="token operator">>=</span> timeoutDuration <span class="token punctuation">{</span>
            cm<span class="token punctuation">.</span><span class="token function">startElection</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
            cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
            <span class="token keyword">return</span>
        <span class="token punctuation">}</span>
        cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>首先通过调用<code>cm.electionTimeout()</code>选择一个（伪）随机的选举超时时间，我们这里根据论文的建议将范围设置为150ms到300ms。像<code>ConsensusModule</code>中的大多数方法一样，<code>runElectionTimer</code>在访问属性时会先锁定结构体对象。这一步是必不可少的，因为我们要尽可能地支持并发，而这也是Go的优势之一。这也意味着代码需要顺序执行，而不能分散到多个事件处理程序中。不过，RPC请求同时也在发生，所以我们必须保护共享数据结构。我们后面会介绍RPC处理器。</p>
<p>主循环中运行了一个周期为10ms的ticker。还有更有效的方法可以实现等待事件，但是使用这种写法的代码是最简单的。每过10ms都会执行一次循环，理论上说定时器可以在整个等待过程中sleep，但是这样会导致服务响应速度下降，而且日志中的调试/跟踪操作会更困难。我们会检查状态是否跟预期一致<sup id="fnref:2"><a href="#fn:2" rel="footnote">2</a></sup>，以及任期有没有改变，如果有任何问题，我们就停止选举定时器。</p>
<p>如果距离上一次”选举重置事件“时间过长，服务器会开始新一轮选举并变成候选人。什么是选举重置事件？可以是任何能够终止选举的事件——比如，收到了有效的心跳信息，为其它候选人投票。我们很快会看到这部分代码。</p>
<h3 id="成为候选者"><a href="#成为候选者" class="headerlink" title="成为候选者"></a>成为候选者</h3><p>前面提到，如果追随者在一段时间内没有收到领导者或其它候选人的信息，它就会开始新一轮的选举。在查看代码之前，我们先思考一下进行选举需要做哪些事情：</p>
<ol>
<li>将状态切换为候选人并增加任期，因为这是算法对每次选举的要求。</li>
<li>发送RV请求给其它同伴，请他们在本轮选举中为自己投票。</li>
<li>等待RPC请求的返回值，并统计我们是否获得足够多的票数成为领导者。</li>
</ol>
<p>在Go语言中，这个逻辑可以在一个函数中完成：</p>
<pre class=" language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>cm <span class="token operator">*</span>ConsensusModule<span class="token punctuation">)</span> <span class="token function">startElection</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  cm<span class="token punctuation">.</span>state <span class="token operator">=</span> Candidate
  cm<span class="token punctuation">.</span>currentTerm <span class="token operator">+=</span> <span class="token number">1</span>
  savedCurrentTerm <span class="token operator">:=</span> cm<span class="token punctuation">.</span>currentTerm
  cm<span class="token punctuation">.</span>electionResetEvent <span class="token operator">=</span> time<span class="token punctuation">.</span><span class="token function">Now</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
  cm<span class="token punctuation">.</span>votedFor <span class="token operator">=</span> cm<span class="token punctuation">.</span>id
  cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"becomes Candidate (currentTerm=%d); log=%v"</span><span class="token punctuation">,</span> savedCurrentTerm<span class="token punctuation">,</span> cm<span class="token punctuation">.</span>log<span class="token punctuation">)</span>

  <span class="token keyword">var</span> votesReceived <span class="token builtin">int32</span> <span class="token operator">=</span> <span class="token number">1</span>

  <span class="token comment" spellcheck="true">// 向其它所有服务器发送RV请求</span>
  <span class="token keyword">for</span> <span class="token boolean">_</span><span class="token punctuation">,</span> peerId <span class="token operator">:=</span> <span class="token keyword">range</span> cm<span class="token punctuation">.</span>peerIds <span class="token punctuation">{</span>
    <span class="token keyword">go</span> <span class="token keyword">func</span><span class="token punctuation">(</span>peerId <span class="token builtin">int</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      args <span class="token operator">:=</span> RequestVoteArgs<span class="token punctuation">{</span>
        Term<span class="token punctuation">:</span>        savedCurrentTerm<span class="token punctuation">,</span>
        CandidateId<span class="token punctuation">:</span> cm<span class="token punctuation">.</span>id<span class="token punctuation">,</span>
      <span class="token punctuation">}</span>
      <span class="token keyword">var</span> reply RequestVoteReply

      cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"sending RequestVote to %d: %+v"</span><span class="token punctuation">,</span> peerId<span class="token punctuation">,</span> args<span class="token punctuation">)</span>
      <span class="token keyword">if</span> err <span class="token operator">:=</span> cm<span class="token punctuation">.</span>server<span class="token punctuation">.</span><span class="token function">Call</span><span class="token punctuation">(</span>peerId<span class="token punctuation">,</span> <span class="token string">"ConsensusModule.RequestVote"</span><span class="token punctuation">,</span> args<span class="token punctuation">,</span> <span class="token operator">&amp;</span>reply<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">==</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span>
        cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token keyword">defer</span> cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"received RequestVoteReply %+v"</span><span class="token punctuation">,</span> reply<span class="token punctuation">)</span>

        <span class="token comment" spellcheck="true">// 状态不是候选人，退出选举（可能退化为追随者，也可能已经胜选成为领导者）</span>
        <span class="token keyword">if</span> cm<span class="token punctuation">.</span>state <span class="token operator">!=</span> Candidate <span class="token punctuation">{</span>
          cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"while waiting for reply, state = %v"</span><span class="token punctuation">,</span> cm<span class="token punctuation">.</span>state<span class="token punctuation">)</span>
          <span class="token keyword">return</span>
        <span class="token punctuation">}</span>

        <span class="token comment" spellcheck="true">// 存在更高任期（新领导者），转换为追随者</span>
        <span class="token keyword">if</span> reply<span class="token punctuation">.</span>Term <span class="token operator">></span> savedCurrentTerm <span class="token punctuation">{</span>
          cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"term out of date in RequestVoteReply"</span><span class="token punctuation">)</span>
          cm<span class="token punctuation">.</span><span class="token function">becomeFollower</span><span class="token punctuation">(</span>reply<span class="token punctuation">.</span>Term<span class="token punctuation">)</span>
          <span class="token keyword">return</span>
        <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> reply<span class="token punctuation">.</span>Term <span class="token operator">==</span> savedCurrentTerm <span class="token punctuation">{</span>
          <span class="token keyword">if</span> reply<span class="token punctuation">.</span>VoteGranted <span class="token punctuation">{</span>
            votes <span class="token operator">:=</span> <span class="token function">int</span><span class="token punctuation">(</span>atomic<span class="token punctuation">.</span><span class="token function">AddInt32</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>votesReceived<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token keyword">if</span> votes<span class="token operator">*</span><span class="token number">2</span> <span class="token operator">></span> <span class="token function">len</span><span class="token punctuation">(</span>cm<span class="token punctuation">.</span>peerIds<span class="token punctuation">)</span><span class="token operator">+</span><span class="token number">1</span> <span class="token punctuation">{</span>
              <span class="token comment" spellcheck="true">// 获得票数超过一半，选举获胜，成为最新的领导者</span>
              cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"wins election with %d votes"</span><span class="token punctuation">,</span> votes<span class="token punctuation">)</span>
              cm<span class="token punctuation">.</span><span class="token function">startLeader</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
              <span class="token keyword">return</span>
            <span class="token punctuation">}</span>
          <span class="token punctuation">}</span>
        <span class="token punctuation">}</span>
      <span class="token punctuation">}</span>
    <span class="token punctuation">}</span><span class="token punctuation">(</span>peerId<span class="token punctuation">)</span>
  <span class="token punctuation">}</span>

  <span class="token comment" spellcheck="true">// 另行启动一个选举定时器，以防本次选举不成功</span>
  <span class="token keyword">go</span> cm<span class="token punctuation">.</span><span class="token function">runElectionTimer</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span></code></pre>
<p>候选人首先给自己投票——将<code>votesReceived</code>初始化为1，并赋值<code>cm.votedFor = cm.id</code>。</p>
<p>然后并行地向所有的同伴发送RPC请求。每个RPC都是在各自的goroutine中完成的，因为我们的RPC调用的同步的——程序会阻塞至收到响应为止，这可能需要一段时间。</p>
<p>这里正好可以演示一下RPC是如何实现的：</p>
<pre class=" language-go"><code class="language-go">cm<span class="token punctuation">.</span>server<span class="token punctuation">.</span><span class="token function">Call</span><span class="token punctuation">(</span>peerId<span class="token punctuation">,</span> <span class="token string">"ConsensusModule.RequestVote"</span><span class="token punctuation">,</span> args<span class="token punctuation">,</span> <span class="token operator">&amp;</span>reply<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>我们使用<code>ConsensusModule.server</code>中保存的<code>Server</code>指针来发起远程调用，并指定<code>ConsensusModule.RequestVotes</code>作为请求的方法名，最终会调用第一个参数指定的同伴服务器中的<code>RequestVote</code>方法。</p>
<p>如果RPC调用成功，因为已经过去了一段时间，我们必须检查服务器状态来决定下一步操作。如果我们的状态不是候选人，退出。什么时候会出现这种情况呢？举例来说，我们可能因为其它RPC请求返回了足够多的票数而胜选成为领导者，或者某个RPC请求从其它服务器收到了更高的任期，因此我们退化为跟随者。一定要要记住，在网络不稳定的情况下，RPC请求可能需要很长时间才能到达——当我们收到回复时，可能其它代码已经继续执行了，在这种情况下优雅地放弃非常重要。</p>
<p>如果收到回复时我们还是候选人状态，先检查回复信息中的任期并与我们发送请求时的任期进行比较。如果返回信息中的任期更高，我们就恢复到追随者状态。例如，我们在收集选票时其它服务器胜选就会出现该情况。</p>
<p>如果返回的任期与我们发送时一致，检查是否赞成投票。我们使用原子变量<code>votes</code>从多个goroutine中安全地收集选票，如果服务器收到了大多数的赞成票（包括自己的赞成票），就变成领导者。</p>
<p>注意这里的<code>startElection</code>方法是非阻塞的。方法中会更新一些状态，启动一批goroutine并返回。因此，还应该在goroutine中启动新的选举定时器——也就是最后一行代码所做的事。这样可以保证，如果<em>本轮</em>选举没有任何结果，在定时结束后会开始新一轮的选举。这也解释了<code>runElectionTimer</code>中的状态检查：如果本轮选举确实将该服务器变成了领导者，那么并发运行的<code>runElectionTimer</code>在观察到服务器状态与期望值不同时会直接返回。</p>
<h3 id="成为领导者"><a href="#成为领导者" class="headerlink" title="成为领导者"></a>成为领导者</h3><p>我们已经看到，当投票结果显示当前服务器胜选时，<code>startElection</code>中会调用<code>startLeader</code>方法，其代码如下：</p>
<pre class=" language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>cm <span class="token operator">*</span>ConsensusModule<span class="token punctuation">)</span> <span class="token function">startLeader</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  cm<span class="token punctuation">.</span>state <span class="token operator">=</span> Leader
  cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"becomes Leader; term=%d, log=%v"</span><span class="token punctuation">,</span> cm<span class="token punctuation">.</span>currentTerm<span class="token punctuation">,</span> cm<span class="token punctuation">.</span>log<span class="token punctuation">)</span>

  <span class="token keyword">go</span> <span class="token keyword">func</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    ticker <span class="token operator">:=</span> time<span class="token punctuation">.</span><span class="token function">NewTicker</span><span class="token punctuation">(</span><span class="token number">50</span> <span class="token operator">*</span> time<span class="token punctuation">.</span>Millisecond<span class="token punctuation">)</span>
    <span class="token keyword">defer</span> ticker<span class="token punctuation">.</span><span class="token function">Stop</span><span class="token punctuation">(</span><span class="token punctuation">)</span>

    <span class="token comment" spellcheck="true">// 只要当前服务器是领导者，就要周期性发送心跳</span>
    <span class="token keyword">for</span> <span class="token punctuation">{</span>
      cm<span class="token punctuation">.</span><span class="token function">leaderSendHeartbeats</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
      <span class="token operator">&lt;-</span>ticker<span class="token punctuation">.</span>C

      cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
      <span class="token keyword">if</span> cm<span class="token punctuation">.</span>state <span class="token operator">!=</span> Leader <span class="token punctuation">{</span>
        cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token keyword">return</span>
      <span class="token punctuation">}</span>
      cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span></code></pre>
<p>这实际上是一个相当简单的方法：所有的内容就是<em>心跳</em>定时器——只要当前的CM是领导者，这个goroutine就会每隔50ms调用一次<code>leaderSendHeartbeats</code>。下面是<code>leaderSendHeartbeats</code>对应的代码：</p>
<pre class=" language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>cm <span class="token operator">*</span>ConsensusModule<span class="token punctuation">)</span> <span class="token function">leaderSendHeartbeats</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
  savedCurrentTerm <span class="token operator">:=</span> cm<span class="token punctuation">.</span>currentTerm
  cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>

  <span class="token comment" spellcheck="true">// 向所有追随者发送AE请求</span>
  <span class="token keyword">for</span> <span class="token boolean">_</span><span class="token punctuation">,</span> peerId <span class="token operator">:=</span> <span class="token keyword">range</span> cm<span class="token punctuation">.</span>peerIds <span class="token punctuation">{</span>
    args <span class="token operator">:=</span> AppendEntriesArgs<span class="token punctuation">{</span>
      Term<span class="token punctuation">:</span>     savedCurrentTerm<span class="token punctuation">,</span>
      LeaderId<span class="token punctuation">:</span> cm<span class="token punctuation">.</span>id<span class="token punctuation">,</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">go</span> <span class="token keyword">func</span><span class="token punctuation">(</span>peerId <span class="token builtin">int</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"sending AppendEntries to %v: ni=%d, args=%+v"</span><span class="token punctuation">,</span> peerId<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> args<span class="token punctuation">)</span>
      <span class="token keyword">var</span> reply AppendEntriesReply
      <span class="token keyword">if</span> err <span class="token operator">:=</span> cm<span class="token punctuation">.</span>server<span class="token punctuation">.</span><span class="token function">Call</span><span class="token punctuation">(</span>peerId<span class="token punctuation">,</span> <span class="token string">"ConsensusModule.AppendEntries"</span><span class="token punctuation">,</span> args<span class="token punctuation">,</span> <span class="token operator">&amp;</span>reply<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">==</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span>
        cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token keyword">defer</span> cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token comment" spellcheck="true">// 如果响应消息中的任期大于当前任期，则表明集群有新的领导者，转换为追随者</span>
        <span class="token keyword">if</span> reply<span class="token punctuation">.</span>Term <span class="token operator">></span> savedCurrentTerm <span class="token punctuation">{</span>
          cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"term out of date in heartbeat reply"</span><span class="token punctuation">)</span>
          cm<span class="token punctuation">.</span><span class="token function">becomeFollower</span><span class="token punctuation">(</span>reply<span class="token punctuation">.</span>Term<span class="token punctuation">)</span>
          <span class="token keyword">return</span>
        <span class="token punctuation">}</span>
      <span class="token punctuation">}</span>
    <span class="token punctuation">}</span><span class="token punctuation">(</span>peerId<span class="token punctuation">)</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>这里的逻辑有点类似于startElection，为每个同伴启动一个goroutine来发送RPC请求。这里的RPC请求是没有日志内容的<code>AppendEntries(AE)</code>，在Raft中扮演心跳的角色。</p>
<p>与处理RV的响应时相同，如果RPC返回的任期高于我们自己的任期值，则当前服务器变为追随者。这里正好查看一下<code>becomeFollower</code>方法：</p>
<pre class=" language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>cm <span class="token operator">*</span>ConsensusModule<span class="token punctuation">)</span> <span class="token function">becomeFollower</span><span class="token punctuation">(</span>term <span class="token builtin">int</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"becomes Follower with term=%d; log=%v"</span><span class="token punctuation">,</span> term<span class="token punctuation">,</span> cm<span class="token punctuation">.</span>log<span class="token punctuation">)</span>
  cm<span class="token punctuation">.</span>state <span class="token operator">=</span> Follower
  cm<span class="token punctuation">.</span>currentTerm <span class="token operator">=</span> term
  cm<span class="token punctuation">.</span>votedFor <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">1</span>
  cm<span class="token punctuation">.</span>electionResetEvent <span class="token operator">=</span> time<span class="token punctuation">.</span><span class="token function">Now</span><span class="token punctuation">(</span><span class="token punctuation">)</span>

  <span class="token comment" spellcheck="true">// 启动选举定时器</span>
  <span class="token keyword">go</span> cm<span class="token punctuation">.</span><span class="token function">runElectionTimer</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span></code></pre>
<p>该方法中首先将CM的状态变为追随者，并重置其任期和其它重要的状态属性。这里还启动了一个新的选举定时器，因为这是每个追随者都要在后台运行的任务。</p>
<h3 id="应答RPC请求"><a href="#应答RPC请求" class="headerlink" title="应答RPC请求"></a>应答RPC请求</h3><p>到目前为止，我们已经看到了实现代码中的<em>主动</em>部分——启动RPC、计时器以及状态转换的部分。但是在我们看到服务器方法（其它同伴远程调用的过程）之前，演示的代码都是不完整的。我们先从<code>RequestVote</code>开始：</p>
<pre class=" language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>cm <span class="token operator">*</span>ConsensusModule<span class="token punctuation">)</span> <span class="token function">RequestVote</span><span class="token punctuation">(</span>args RequestVoteArgs<span class="token punctuation">,</span> reply <span class="token operator">*</span>RequestVoteReply<span class="token punctuation">)</span> <span class="token builtin">error</span> <span class="token punctuation">{</span>
  cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
  <span class="token keyword">defer</span> cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
  <span class="token keyword">if</span> cm<span class="token punctuation">.</span>state <span class="token operator">==</span> Dead <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token boolean">nil</span>
  <span class="token punctuation">}</span>
  cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"RequestVote: %+v [currentTerm=%d, votedFor=%d]"</span><span class="token punctuation">,</span> args<span class="token punctuation">,</span> cm<span class="token punctuation">.</span>currentTerm<span class="token punctuation">,</span> cm<span class="token punctuation">.</span>votedFor<span class="token punctuation">)</span>

  <span class="token comment" spellcheck="true">// 请求中的任期大于本地任期，转换为追随者状态</span>
  <span class="token keyword">if</span> args<span class="token punctuation">.</span>Term <span class="token operator">></span> cm<span class="token punctuation">.</span>currentTerm <span class="token punctuation">{</span>
    cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"... term out of date in RequestVote"</span><span class="token punctuation">)</span>
    cm<span class="token punctuation">.</span><span class="token function">becomeFollower</span><span class="token punctuation">(</span>args<span class="token punctuation">.</span>Term<span class="token punctuation">)</span>
  <span class="token punctuation">}</span>

  <span class="token comment" spellcheck="true">// 任期相同，且未投票或已投票给当前请求同伴，则返回赞成投票；否则，返回反对投票。</span>
  <span class="token keyword">if</span> cm<span class="token punctuation">.</span>currentTerm <span class="token operator">==</span> args<span class="token punctuation">.</span>Term <span class="token operator">&amp;&amp;</span>
    <span class="token punctuation">(</span>cm<span class="token punctuation">.</span>votedFor <span class="token operator">==</span> <span class="token operator">-</span><span class="token number">1</span> <span class="token operator">||</span> cm<span class="token punctuation">.</span>votedFor <span class="token operator">==</span> args<span class="token punctuation">.</span>CandidateId<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    reply<span class="token punctuation">.</span>VoteGranted <span class="token operator">=</span> <span class="token boolean">true</span>
    cm<span class="token punctuation">.</span>votedFor <span class="token operator">=</span> args<span class="token punctuation">.</span>CandidateId
    cm<span class="token punctuation">.</span>electionResetEvent <span class="token operator">=</span> time<span class="token punctuation">.</span><span class="token function">Now</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
    reply<span class="token punctuation">.</span>VoteGranted <span class="token operator">=</span> <span class="token boolean">false</span>
  <span class="token punctuation">}</span>
  reply<span class="token punctuation">.</span>Term <span class="token operator">=</span> cm<span class="token punctuation">.</span>currentTerm
  cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"... RequestVote reply: %+v"</span><span class="token punctuation">,</span> reply<span class="token punctuation">)</span>
  <span class="token keyword">return</span> <span class="token boolean">nil</span>
<span class="token punctuation">}</span></code></pre>
<p>注意这里检查了”dead“状态，稍后会讨论这一点。</p>
<p>首先是一段熟悉的逻辑，检查任期是否过时并转换为追随者。如果它已经是一个追随者，状态不会改变但是其它状态属性会重置。</p>
<p>否则，如果调用者的任期与我们一致，而且我们尚未给其它候选人投票，那我们就赞成该选票。我们决不会向<em>旧任期</em>发起的RPC请求投票。</p>
<p>下面是<code>AppendEntries</code>的代码：</p>
<pre class=" language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>cm <span class="token operator">*</span>ConsensusModule<span class="token punctuation">)</span> <span class="token function">AppendEntries</span><span class="token punctuation">(</span>args AppendEntriesArgs<span class="token punctuation">,</span> reply <span class="token operator">*</span>AppendEntriesReply<span class="token punctuation">)</span> <span class="token builtin">error</span> <span class="token punctuation">{</span>
  cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
  <span class="token keyword">defer</span> cm<span class="token punctuation">.</span>mu<span class="token punctuation">.</span><span class="token function">Unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
  <span class="token keyword">if</span> cm<span class="token punctuation">.</span>state <span class="token operator">==</span> Dead <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token boolean">nil</span>
  <span class="token punctuation">}</span>
  cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"AppendEntries: %+v"</span><span class="token punctuation">,</span> args<span class="token punctuation">)</span>

  <span class="token comment" spellcheck="true">// 请求中的任期大于本地任期，转换为追随者状态</span>
  <span class="token keyword">if</span> args<span class="token punctuation">.</span>Term <span class="token operator">></span> cm<span class="token punctuation">.</span>currentTerm <span class="token punctuation">{</span>
    cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"... term out of date in AppendEntries"</span><span class="token punctuation">)</span>
    cm<span class="token punctuation">.</span><span class="token function">becomeFollower</span><span class="token punctuation">(</span>args<span class="token punctuation">.</span>Term<span class="token punctuation">)</span>
  <span class="token punctuation">}</span>

  reply<span class="token punctuation">.</span>Success <span class="token operator">=</span> <span class="token boolean">false</span>
  <span class="token keyword">if</span> args<span class="token punctuation">.</span>Term <span class="token operator">==</span> cm<span class="token punctuation">.</span>currentTerm <span class="token punctuation">{</span>
    <span class="token comment" spellcheck="true">// 如果当前状态不是追随者，则变为追随者</span>
    <span class="token keyword">if</span> cm<span class="token punctuation">.</span>state <span class="token operator">!=</span> Follower <span class="token punctuation">{</span>
      cm<span class="token punctuation">.</span><span class="token function">becomeFollower</span><span class="token punctuation">(</span>args<span class="token punctuation">.</span>Term<span class="token punctuation">)</span>
    <span class="token punctuation">}</span>
    cm<span class="token punctuation">.</span>electionResetEvent <span class="token operator">=</span> time<span class="token punctuation">.</span><span class="token function">Now</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    reply<span class="token punctuation">.</span>Success <span class="token operator">=</span> <span class="token boolean">true</span>
  <span class="token punctuation">}</span>

  reply<span class="token punctuation">.</span>Term <span class="token operator">=</span> cm<span class="token punctuation">.</span>currentTerm
  cm<span class="token punctuation">.</span><span class="token function">dlog</span><span class="token punctuation">(</span><span class="token string">"AppendEntries reply: %+v"</span><span class="token punctuation">,</span> <span class="token operator">*</span>reply<span class="token punctuation">)</span>
  <span class="token keyword">return</span> <span class="token boolean">nil</span>
<span class="token punctuation">}</span></code></pre>
<p>这里的逻辑也跟论文的图2中的选主部分一致，需要理解的一个复杂点在于：</p>
<pre class=" language-go"><code class="language-go"><span class="token keyword">if</span> cm<span class="token punctuation">.</span>state <span class="token operator">!=</span> Follower <span class="token punctuation">{</span>
  cm<span class="token punctuation">.</span><span class="token function">becomeFollower</span><span class="token punctuation">(</span>args<span class="token punctuation">.</span>Term<span class="token punctuation">)</span>
<span class="token punctuation">}</span></code></pre>
<p><strong>Q：</strong>如果服务器是领导者呢——为什么要变成其它领导者的追随者？</p>
<p><strong>A：</strong>Raft协议保证了在任一给定的任期都只有唯一的领导者。如果你自己研究<code>RequestVote</code>的逻辑，以及<code>startElection</code>中发送RV请求的代码，你会发现在集群中不会有两个使用相同任期的领导者存在。这个条件对于那些发现其它同伴赢得本轮选举的<em>候选人</em>很重要。</p>
<h3 id="状态和goroutine"><a href="#状态和goroutine" class="headerlink" title="状态和goroutine"></a>状态和goroutine</h3><p>我们有必要回顾一下CM中可能存在的所有状态，以及其对应运行的不同goroutine：</p>
<p><strong>追随者：</strong>当CM被初始化为追随者，或者每次执行<code>becomeFollower</code>方法时，都会启动新的goroutine运行<code>runElectionTimer</code>，这是追随者的附属操作。请注意，在短时间内可能同时运行多个选举定时器。假设一个追随者收到了领导者发出的带有更高任期的RV请求，这将触发一次<code>becomeFollower</code>调用并启动一个新的定时器goroutine。但是旧的goroutine只要注意到任期的变化就会自然退出。</p>
<p><strong>候选人：</strong>候选人也有一个并行运行的选举定时器goroutine，但是除此之外，它还有一些发送RPC请求的goroutine。它与追随者具有相同的保护措施，可以在新选举开始时停止”旧“的选举goroutine。一定要记住，RPC goroutine可能需要很长时间才能完成，因此，如果RPC调用返回时，它们发现自身的任期已经过时，那么它们必须安静地退出。</p>
<p><strong>领导者：</strong>领导者没有选举定时goroutine，但是它肯定有一个每隔50ms执行一次的心跳goroutine。</p>
<p>代码中还有一个附加的状态——<strong>Dead</strong>状态。这纯粹是为了有序关闭CM。调用”Stop“方法会将状态置为Dead，所有的goroutine在观察到该状态后会立即退出。</p>
<p>这些goroutine的运行可能会让人担忧——如果其中一些goroutine滞留在后台，该怎么办？或者出现更糟的情况，这些goroutine不断泄漏而且数量无限制地增长，怎么办？这也就是泄漏检查的目的，而且一些测试案例中也启用了泄漏检查。这些测试中会执行非常规的一系列Raft选举操作，并保证在测试结束后没有任何游离的goroutine在运行（在调用<code>stop</code>方法之后，给这些goroutine一些时间去退出）。</p>
<h3 id="服务器失控和增加任期"><a href="#服务器失控和增加任期" class="headerlink" title="服务器失控和增加任期"></a>服务器失控和增加任期</h3><p>作为这一部分的总结，我们来研究一个可能出现的复杂场景以及Raft如何应对。我觉得这个例子很有趣，也很有启发性。这里我试图以故事的方式来呈现，但是你最好有一张纸来记录各服务器的状态。如果你没法理解这个示例——请发邮件告知我，我很乐意将它改得更清楚一些。</p>
<p>想象一个有三台服务器A，B和C的集群。假设A是领导者，起始任期是1，并且集群正在完美运行着。A每隔50ms都想B、C发一次心跳AE请求，并在几毫秒内得到及时响应。每一次的AE请求都会重置B、C中的<code>electionResetEvent</code>属性，因此它们也都很愿意继续做追随者。</p>
<p>在某个时刻，由于网络路由器的临时故障，服务器B与A、C之间出现了网络分区。A仍然每隔50ms发一次AE请求，但是这些AE要求要么立即失败，或者是由于底层RPC引擎的超时导致失败。A对此无能为力，但是问题也不大。我们目前还没有涉及到日志复制，但是因为3台服务器中的2台都是正常的，集群仍然可以提交客户端指令。</p>
<p>那么B呢？假设在断开连接的时候，它的选举超时设置为了200ms。在断开连接大约200ms后，B的<code>runElectionTimer</code>会意识到在选举等待时间内没有收到领导者的信息，B无法区分是谁出了错，所以它就变为了候选者并开启一轮选举。</p>
<p>因此B的任期将变为2（而A和C的任期仍然是1）。B会向A和C发送RV请求，要求他们为自己投票；当然，这些请求会丢失在网络中。不要惊慌！B中的<code>startElection</code>方法也启动了另一个goroutine执行<code>runElectionTimer</code>任务，假设这个goroutine会等待250ms（要记住，我们的超时时间是在150ms-300ms之间随机选择的），以查看上一轮选举是否出现实质性的结果。因为B仍然被完全隔离，也就不会发生什么，因此<code>runElectionTimer</code>会发起<em>另一轮</em>选举，并将任期增加到3。</p>
<p>如此这般，B的服务器在几秒钟之后自我重置并重新上线，与此同时，B由于每隔一段时间都发起选举，它的任期已经变为8。</p>
<p>这时网络分区问题已经修复，B重新连接到了A和C。</p>
<p>不久之后，A发送的AE请求到了。回想一下，A每隔50ms都会发送心跳信息，即使B一直没有回复。</p>
<p>B的<code>AppendEntries</code>被调用，并且回复信息中携带的任务为8.</p>
<p>A在<code>leaderSendHeartbeats</code>方法中收到此回复，检查回复信息中的任期后发起比自己的任期更高。A将自身的任期改为8并变成追随者。集群暂时失去了领导者。</p>
<p>接下来根据定时的不同，可能会出现多种情况。B是候选者，但是它可能在网络恢复之前已经发送了RV请求；C是追随者，但是由于在选举超时内没有收到A的AE请求，也会变成候选人；A变成了追随者，也可能因为选举超时变成候选人。</p>
<p>所以其中任何一个服务器都可能在下一轮的选举中胜选，注意，这只是因为我们在这里并没有复制任何日志。我们将在下一部分看到，实际情况下，A和C可能会在B离线的时候添加了一些写的客户端指令，因此它们的日志是最新的。因此，B不会变成新的领导者——会出现新的一轮选举，而且A或C会胜选。在下一部分我们会再次讨论这个场景。</p>
<p>假如在B断开连接之后没有新增任何指令，则重新连接之后更换领导者也是完全可以的。</p>
<p>看起来可能有些效率低下——确实如此。这里更换领导者是不必要的，因为A在整个场景中都是非常健康的。但是，以牺牲特殊情况下的效率为代价，保证算法逻辑的简单性，这也是Raft做出的选择之一。算法在一般情形（没有任何异常）下的效率更重要，因为集群99.9%的时间都处于该状态。</p>
<h3 id="下一步"><a href="#下一步" class="headerlink" title="下一步"></a>下一步</h3><p>为了确保你对实现的理解不仅仅局限在理论，我强烈建议你运行一下<a target="_blank" rel="noopener" href="https://github.com/eliben/raft/tree/master/part1">代码</a>。</p>
<p>代码库中的<code>README</code>文件对于代码交互、运行测试用例、观察结果提供了详细的说明。代码中附带了很多针对特定场景的测试（包括前面章节中提到的场景），运行一个测试用例并查看Raft日志对于学习很有意义。注意到代码中调用的<code>cm.dlog(...)</code>了吗？仓库中提供了一个工具可以将这些日志在HTML文件中进行可视化——可以在<code>README</code>文件中查看说明。运行代码，查看日志，也可以在代码中随意添加自己的<code>dlog</code>，以便更好地理解代码中的不同部分是在何时运行的。</p>
<p>本系统的<a href="/posts/25bc8c4b.html" title="第2部分">第2部分</a>会描述更完整的Raft实现，其中处理了客户端的指令，并在集群中复制这些日志。敬请关注！</p>
<h3 id="附："><a href="#附：" class="headerlink" title="附："></a>附：</h3><p>Raft论文中的图2如下所示，这里对其做简要的翻译及说明。其中有部分关于日志复制和提交的，可以在看完下一篇之后重新对照理解。</p>
<p><img src="/posts/be586517/Raft-RPC%E5%8F%82%E6%95%B0.png" alt="Raft-RPC参数"></p>
<h4 id="状态-State"><a href="#状态-State" class="headerlink" title="状态 State"></a>状态 State</h4><p>服务器中的状态字段有三类，分别进行介绍。</p>
<p>所有服务器中都需要持久化保存的状态（在响应RPC请求之前需要更新到稳定的存储介质中）</p>
<table>
<thead>
<tr>
<th>字段</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>currentTerm</td>
<td>服务器接收到的最新任期（启动时初始化为0，单调递增）</td>
</tr>
<tr>
<td>votedFor</td>
<td>在当前任期内收到赞同票的候选人ID（如果没有就是null）</td>
</tr>
<tr>
<td>log[]</td>
<td>日志条目；每个条目中 包含输入状态机的指令，以及领导者接收条目时的任期（第一个索引是1）</td>
</tr>
</tbody></table>
<p>所有服务器中经常修改的状态字段：</p>
<table>
<thead>
<tr>
<th>字段</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>commitIndex</td>
<td>确认已提交的日志条目的最大索引值（初始化为0，单调递增）</td>
</tr>
<tr>
<td>lastApplied</td>
<td>应用于状态机的日志条目的最大索引值（初始化为0，单调递增）</td>
</tr>
</tbody></table>
<p>领导者服务器中经常修改的状态字段（选举之后重新初始化）：</p>
<table>
<thead>
<tr>
<th>字段</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>nextIndex[]</td>
<td>对于每个服务器，存储要发送给该服务器的下一条日志条目的索引（初始化为领导者的最新日志索引+1）</td>
</tr>
<tr>
<td>matchIndex[]</td>
<td>对于每个服务器，存储确认复制到该服务器的日志条目的最大索引值（初始化为0，单调递增）</td>
</tr>
</tbody></table>
<h4 id="AE请求"><a href="#AE请求" class="headerlink" title="AE请求"></a>AE请求</h4><p>AE请求即<code>AppendRntries</code>请求，由领导者发起，用于向追随者复制客户端指令，也用于维护心跳。</p>
<h5 id="请求参数"><a href="#请求参数" class="headerlink" title="请求参数"></a>请求参数</h5><table>
<thead>
<tr>
<th>参数</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>term</td>
<td>领导者的任期</td>
</tr>
<tr>
<td>leaderId</td>
<td>领导者ID，追随者就可以对客户端进行重定向</td>
</tr>
<tr>
<td>prevLogIndex</td>
<td>紧接在新日志条目之前的条目的索引</td>
</tr>
<tr>
<td>prevLogTerm</td>
<td><code>prevLogIndex</code>对应条目的任期</td>
</tr>
<tr>
<td>entries[]</td>
<td>需要报错的日志条目（为空时是心跳请求；为了高效可能会发送多条日志）</td>
</tr>
<tr>
<td>leaderCommit</td>
<td>领导者的<code>commitIndex</code></td>
</tr>
</tbody></table>
<h5 id="返回值"><a href="#返回值" class="headerlink" title="返回值"></a>返回值</h5><table>
<thead>
<tr>
<th>参数</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>term</td>
<td>currentTerm，当前任期，回复给领导者。领导者用于自我更新</td>
</tr>
<tr>
<td>success</td>
<td>如果追随者保存了<code>prevLogIndex</code>和<code>prevLogTerm</code>相匹配的日志条目，则返回<code>true</code></td>
</tr>
</tbody></table>
<h5 id="接收方实现："><a href="#接收方实现：" class="headerlink" title="接收方实现："></a>接收方实现：</h5><ol>
<li>如果<code>term &lt; currentTerm </code>，返回<code>false</code>；</li>
<li>如果日志中<code>prevLogIndex</code>对应条目的任期与<code>prevLogTerm</code>不匹配，返回<code>false</code>；</li>
<li>如果本地已存在的日志条目与新的日志冲突（索引相同，但是任期不同），删除本地已存在的条目及其后所有的条目；</li>
<li>追加所有log中未保存的新条目；</li>
<li>如果<code> leaderCommit &gt; commitIndex</code>，就将<code> commitIndex</code>设置为<code>leaderCommit</code>和最新条目的索引中的较小值。</li>
</ol>
<h4 id="RV请求"><a href="#RV请求" class="headerlink" title="RV请求"></a>RV请求</h4><p>候选人执行，用于在发起选举时收集选票。</p>
<h5 id="请求参数-1"><a href="#请求参数-1" class="headerlink" title="请求参数"></a>请求参数</h5><table>
<thead>
<tr>
<th>字段</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>term</td>
<td>候选人的任期</td>
</tr>
<tr>
<td>candidateId</td>
<td>请求选票的候选人ID</td>
</tr>
<tr>
<td>lastLogIndex</td>
<td>候选人的最新日志条目对应索引</td>
</tr>
<tr>
<td>lastLogTerm</td>
<td>候选人的最新日志条目对应任期</td>
</tr>
</tbody></table>
<h5 id="返回值-1"><a href="#返回值-1" class="headerlink" title="返回值"></a>返回值</h5><table>
<thead>
<tr>
<th>字段</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>term</td>
<td>currentTerm，当前任期，回复给候选人。候选人用于自我更新</td>
</tr>
<tr>
<td>voteGranted</td>
<td>true表示候选人获得了赞成票</td>
</tr>
</tbody></table>
<h5 id="接收方实现：-1"><a href="#接收方实现：-1" class="headerlink" title="接收方实现："></a>接收方实现：</h5><ol>
<li>如果<code>term &lt; currentTerm</code>返回<code>false</code>；</li>
<li>如果<code>votedFor</code>为空或等于<code>candidateId</code>，并且候选人的日志至少与接收方的日志一样新，投出赞成票。</li>
</ol>
<h4 id="服务器响应规则"><a href="#服务器响应规则" class="headerlink" title="服务器响应规则"></a>服务器响应规则</h4><p>按照服务器当下的状态（所处的角色），分别进行介绍：</p>
<h5 id="所有服务器："><a href="#所有服务器：" class="headerlink" title="所有服务器："></a>所有服务器：</h5><ul>
<li>如果<code> commitIndex &gt; lastApplied</code>：增加<code>lastApplied</code>，将<code>log[lastApplied]</code>应用到状态机；</li>
<li>如果RPC请求或者响应中携带的任期T满足<code>T &gt; currentTerm</code>：设置<code>currentTerm = T</code>，转换为追随者。</li>
</ul>
<h5 id="追随者："><a href="#追随者：" class="headerlink" title="追随者："></a>追随者：</h5><ul>
<li>响应候选人和领导者的RPC请求；</li>
<li>如果超时等待时间内没有收到当前领导者的AE请求或者给候选人投出选票：转换为候选人。</li>
</ul>
<h5 id="候选人："><a href="#候选人：" class="headerlink" title="候选人："></a>候选人：</h5><ul>
<li>刚转换为候选人时，启动选举：<ul>
<li>增加当前任期，<code>currentTerm</code></li>
<li>给自己投票</li>
<li>重置选举定时器</li>
<li>向其它所有服务器发送RV请求</li>
</ul>
</li>
<li>如果接收到多数服务器的赞成票：变成领导者</li>
<li>如果接收到新领导者发出的AE请求：转换为追随者</li>
<li>如果选举等待超时：开始新一轮选举</li>
</ul>
<h5 id="领导者："><a href="#领导者：" class="headerlink" title="领导者："></a>领导者：</h5><ul>
<li>当选时：向每个服务器发送初始化的空AE请求（心跳）；在空闲时间也重复发送AE请求，防止追随者出现等待超时；</li>
<li>如果从客户端接收到指令：向本地日志中追加条目，在新指令被应用到状态机之后响应客户端；</li>
<li>如果最新的日志索引index与追随者下一条日志索引nextIndex 满足 <code> index ≥ nextIndex</code>：向追随者发送AE请求，携带从<code>nextIndex</code>开始的所有日志条目：<ul>
<li>如果成功：更新追随者对应的<code>nextIndex</code>和<code>matchIndex</code>；</li>
<li>如果AE由于日志不一致而失败：减小<code>nextIndex</code>并重试；</li>
</ul>
</li>
<li>如果存在N，满足<code> N &gt; commitIndex</code>，多数的<code>matchIndex[i] ≥ N</code>，并且<code>log[N].term == currentTerm</code>：设置<code> commitIndex = N</code></li>
</ul>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">1.</span><span style="display: inline-block; vertical-align: top;">这张示意图与Raft论文中的图4是相同的。正好也可以提醒一下，在本系列的文章中。我都假设您已经读过这篇论文了。</span><a href="#fnref:1" rev="footnote"> ↩</a></li><li id="fn:2"><span style="display: inline-block; vertical-align: top; padding-right: 10px;">2.</span><span style="display: inline-block; vertical-align: top;">检查状态是否追随者和候选人可能看起来有点奇怪。难道服务器可以不通过<code>runElectionTimer</code>发起的选举而突然成为领导者吗？ 继续往后阅读了解候选人是如何重启选举计数器的。</span><a href="#fnref:2" rev="footnote"> ↩</a></li></ol></div></div>
            </div>
            <hr/>

            

    <div class="reprint" id="reprint-statement">
        
            <div class="reprint__author">
                <span class="reprint-meta" style="font-weight: bold;">
                    <i class="fas fa-user">
                        文章作者:
                    </i>
                </span>
                <span class="reprint-info">
                    <a href="/about" rel="external nofollow noreferrer">Guo Yaxiang</a>
                </span>
            </div>
            <div class="reprint__type">
                <span class="reprint-meta" style="font-weight: bold;">
                    <i class="fas fa-link">
                        文章链接:
                    </i>
                </span>
                <span class="reprint-info">
                    <a href="https://www.codinghonest.space/posts/be586517.html">https://www.codinghonest.space/posts/be586517.html</a>
                </span>
            </div>
            <div class="reprint__notice">
                <span class="reprint-meta" style="font-weight: bold;">
                    <i class="fas fa-copyright">
                        版权声明:
                    </i>
                </span>
                <span class="reprint-info">
                    本博客所有文章除特別声明外，均采用
                    <a href="https://creativecommons.org/licenses/by/4.0/deed.zh" rel="external nofollow noreferrer" target="_blank">CC BY 4.0</a>
                    许可协议。转载请注明来源
                    <a href="/about" target="_blank">Guo Yaxiang</a>
                    !
                </span>
            </div>
        
    </div>

    <script async defer>
      document.addEventListener("copy", function (e) {
        let toastHTML = '<span>复制成功，请遵循本文的转载规则</span><button class="btn-flat toast-action" onclick="navToReprintStatement()" style="font-size: smaller">查看</a>';
        M.toast({html: toastHTML})
      });

      function navToReprintStatement() {
        $("html, body").animate({scrollTop: $("#reprint-statement").offset().top - 80}, 800);
      }
    </script>



            <div class="tag_share" style="display: block;">
                <div class="post-meta__tag-list" style="display: inline-block;">
                    
                        <div class="article-tag">
                            
                                <a href="/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7/">
                                    <span class="chip bg-color">分布式一致性</span>
                                </a>
                            
                                <a href="/tags/Raft/">
                                    <span class="chip bg-color">Raft</span>
                                </a>
                            
                                <a href="/tags/%E8%AF%91%E6%96%87/">
                                    <span class="chip bg-color">译文</span>
                                </a>
                            
                        </div>
                    
                </div>
                <div class="post_share" style="zoom: 80%; width: fit-content; display: inline-block; float: right; margin: -0.15rem 0;">
                    <link rel="stylesheet" type="text/css" href="/libs/share/css/share.min.css">
<div id="article-share">

    
    <div class="social-share" data-sites="twitter,facebook,google,qq,qzone,wechat,weibo,douban,linkedin" data-wechat-qrcode-helper="<p>微信扫一扫即可分享！</p>"></div>
    <script src="/libs/share/js/social-share.min.js"></script>
    

    

</div>

                </div>
            </div>
            
                <style>
    #reward {
        margin: 40px 0;
        text-align: center;
    }

    #reward .reward-link {
        font-size: 1.4rem;
        line-height: 38px;
    }

    #reward .btn-floating:hover {
        box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2), 0 5px 15px rgba(0, 0, 0, 0.2);
    }

    #rewardModal {
        width: 320px;
        height: 350px;
    }

    #rewardModal .reward-title {
        margin: 15px auto;
        padding-bottom: 5px;
    }

    #rewardModal .modal-content {
        padding: 10px;
    }

    #rewardModal .close {
        position: absolute;
        right: 15px;
        top: 15px;
        color: rgba(0, 0, 0, 0.5);
        font-size: 1.3rem;
        line-height: 20px;
        cursor: pointer;
    }

    #rewardModal .close:hover {
        color: #ef5350;
        transform: scale(1.3);
        -moz-transform:scale(1.3);
        -webkit-transform:scale(1.3);
        -o-transform:scale(1.3);
    }

    #rewardModal .reward-tabs {
        margin: 0 auto;
        width: 210px;
    }

    .reward-tabs .tabs {
        height: 38px;
        margin: 10px auto;
        padding-left: 0;
    }

    .reward-content ul {
        padding-left: 0 !important;
    }

    .reward-tabs .tabs .tab {
        height: 38px;
        line-height: 38px;
    }

    .reward-tabs .tab a {
        color: #fff;
        background-color: #ccc;
    }

    .reward-tabs .tab a:hover {
        background-color: #ccc;
        color: #fff;
    }

    .reward-tabs .wechat-tab .active {
        color: #fff !important;
        background-color: #22AB38 !important;
    }

    .reward-tabs .alipay-tab .active {
        color: #fff !important;
        background-color: #019FE8 !important;
    }

    .reward-tabs .reward-img {
        width: 210px;
        height: 210px;
    }
</style>

<div id="reward">
    <a href="#rewardModal" class="reward-link modal-trigger btn-floating btn-medium waves-effect waves-light red">赏</a>

    <!-- Modal Structure -->
    <div id="rewardModal" class="modal">
        <div class="modal-content">
            <a class="close modal-close"><i class="fas fa-times"></i></a>
            <h4 class="reward-title">你的赏识是我前进的动力</h4>
            <div class="reward-content">
                <div class="reward-tabs">
                    <ul class="tabs row">
                        <li class="tab col s6 alipay-tab waves-effect waves-light"><a href="#alipay">支付宝</a></li>
                        <li class="tab col s6 wechat-tab waves-effect waves-light"><a href="#wechat">微 信</a></li>
                    </ul>
                    <div id="alipay">
                        <img src="/medias/reward/alipay.png" class="reward-img" alt="支付宝打赏二维码">
                    </div>
                    <div id="wechat">
                        <img src="/medias/reward/wechat.png" class="reward-img" alt="微信打赏二维码">
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
    $(function () {
        $('.tabs').tabs();
    });
</script>

            
        </div>
    </div>

    

    

    

    

    

    
        <style>
    .mvaline-card {
        margin: 1.5rem auto;
    }

    .mvaline-card .card-content {
        padding: 20px 20px 5px 20px;
    }
</style>

<div class="card mvaline-card" data-aos="fade-up">
    <div class="comment_headling" style="font-size: 20px; font-weight: 700; position: relative; padding-left: 20px; top: 15px; padding-bottom: 5px;">
        <i class="fas fa-comments fa-fw" aria-hidden="true"></i>
        <span>评论</span>
    </div>
    <div id="mvcomments" class="card-content" style="display: grid">
    </div>
</div>

<script src="/libs/minivaline/MiniValine.js"></script>
<script>
    new MiniValine({
        el: '#mvcomments',
        appId: 'oqAo7rCWejwTqHjGC2APTVDX-MdYXbMMI',
        appKey: 'pfyY919tO7iJaDm7lFmvh5Wp',
        mode: 'DesertsP',
        placeholder: 'Write a Comment',
        pathname: window.location.pathname,
        lang: '',
        adminEmailMd5: '8BBC105436DAAF8C803841C8FD13CD0C',
        tagMeta: ["管理员", "小伙伴", "访客"],
        master: ["8BBC105436DAAF8C803841C8FD13CD0C"],
        friends: ["FA21C7921CD42FE9523E957870873F64", "586BAB41BB5692877F7714DFE4947DEC"],
        math: true,
        md: true,
        enableQQ: false,
        NoRecordIP: false,
        visitor: true,
        maxNest: 6,
        pageSize: 6,
        serverURLs: '',
        emoticonUrl: ["https://cdn.jsdelivr.net/npm/alus@latest", "https://cdn.jsdelivr.net/gh/MiniValine/qq@latest", "https://cdn.jsdelivr.net/gh/MiniValine/Bilibilis@latest", "https://cdn.jsdelivr.net/gh/MiniValine/tieba@latest", "https://cdn.jsdelivr.net/gh/MiniValine/twemoji@latest", "https://cdn.jsdelivr.net/gh/MiniValine/weibo@latest"],
    });
</script>

    

    

    

<article id="prenext-posts" class="prev-next articles">
    <div class="row article-row">
        
        <div class="article col s12 m6" data-aos="fade-up">
            <div class="article-badge left-badge text-color">
                <i class="fas fa-chevron-left"></i>&nbsp;上一篇</div>
            <div class="card">
                <a href="/posts/25bc8c4b.html">
                    <div class="card-image">
                        
                        
                        <img src="/medias/featureimages/8.jpg" class="responsive-img" alt="实现Raft协议：Part 2 - 指令和日志复制">
                        
                        <span class="card-title">实现Raft协议：Part 2 - 指令和日志复制</span>
                    </div>
                </a>
                <div class="card-content article-content">
                    <div class="summary block-with-text">
                        
                            本系列文章的写作目的，在于描述Raft协议的一个功能完备且经过严格测试的实现方式，并提供一些Raft工作方式的直观理解。
                        
                    </div>
                    <div class="publish-info">
                        <span class="publish-date">
                            <i class="far fa-clock fa-fw icon-date"></i>2021-03-04
                        </span>
                        <span class="publish-author">
                            
                            <i class="fas fa-bookmark fa-fw icon-category"></i>
                            
                            <a href="/categories/%E5%88%86%E5%B8%83%E5%BC%8F/" class="post-category">
                                    分布式
                                </a>
                            
                            <a href="/categories/%E5%88%86%E5%B8%83%E5%BC%8F/%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE/" class="post-category">
                                    一致性协议
                                </a>
                            
                            <a href="/categories/%E5%88%86%E5%B8%83%E5%BC%8F/%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE/Raft/" class="post-category">
                                    Raft
                                </a>
                            
                            
                        </span>
                    </div>
                </div>
                
                <div class="card-action article-tags">
                    
                    <a href="/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7/">
                        <span class="chip bg-color">分布式一致性</span>
                    </a>
                    
                    <a href="/tags/Raft/">
                        <span class="chip bg-color">Raft</span>
                    </a>
                    
                    <a href="/tags/%E8%AF%91%E6%96%87/">
                        <span class="chip bg-color">译文</span>
                    </a>
                    
                </div>
                
            </div>
        </div>
        
        
        <div class="article col s12 m6" data-aos="fade-up">
            <div class="article-badge right-badge text-color">
                下一篇&nbsp;<i class="fas fa-chevron-right"></i>
            </div>
            <div class="card">
                <a href="/posts/772d24e8.html">
                    <div class="card-image">
                        
                        
                        <img src="/medias/featureimages/6.jpg" class="responsive-img" alt="实现Raft协议：Part 0 - 介绍">
                        
                        <span class="card-title">实现Raft协议：Part 0 - 介绍</span>
                    </div>
                </a>
                <div class="card-content article-content">
                    <div class="summary block-with-text">
                        
                            本系列文章的写作目的，在于描述Raft协议的一个功能完备且经过严格测试的实现方式，并提供一些Raft工作方式的直观理解。
                        
                    </div>
                    <div class="publish-info">
                            <span class="publish-date">
                                <i class="far fa-clock fa-fw icon-date"></i>2021-03-04
                            </span>
                        <span class="publish-author">
                            
                            <i class="fas fa-bookmark fa-fw icon-category"></i>
                            
                            <a href="/categories/%E5%88%86%E5%B8%83%E5%BC%8F/" class="post-category">
                                    分布式
                                </a>
                            
                            <a href="/categories/%E5%88%86%E5%B8%83%E5%BC%8F/%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE/" class="post-category">
                                    一致性协议
                                </a>
                            
                            <a href="/categories/%E5%88%86%E5%B8%83%E5%BC%8F/%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE/Raft/" class="post-category">
                                    Raft
                                </a>
                            
                            
                        </span>
                    </div>
                </div>
                
                <div class="card-action article-tags">
                    
                    <a href="/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7/">
                        <span class="chip bg-color">分布式一致性</span>
                    </a>
                    
                    <a href="/tags/Raft/">
                        <span class="chip bg-color">Raft</span>
                    </a>
                    
                    <a href="/tags/%E8%AF%91%E6%96%87/">
                        <span class="chip bg-color">译文</span>
                    </a>
                    
                </div>
                
            </div>
        </div>
        
    </div>
</article>

</div>


<script>
    $('#articleContent').on('copy', function (e) {
        // IE8 or earlier browser is 'undefined'
        if (typeof window.getSelection === 'undefined') return;

        var selection = window.getSelection();
        // if the selection is short let's not annoy our users.
        if (('' + selection).length < Number.parseInt('120')) {
            return;
        }

        // create a div outside of the visible area and fill it with the selected text.
        var bodyElement = document.getElementsByTagName('body')[0];
        var newdiv = document.createElement('div');
        newdiv.style.position = 'absolute';
        newdiv.style.left = '-99999px';
        bodyElement.appendChild(newdiv);
        newdiv.appendChild(selection.getRangeAt(0).cloneContents());

        // we need a <pre> tag workaround.
        // otherwise the text inside "pre" loses all the line breaks!
        if (selection.getRangeAt(0).commonAncestorContainer.nodeName === 'PRE') {
            newdiv.innerHTML = "<pre>" + newdiv.innerHTML + "</pre>";
        }

        var url = document.location.href;
        newdiv.innerHTML += '<br />'
            + '来源: 码道诚公<br />'
            + '文章作者: Guo Yaxiang<br />'
            + '文章链接: <a href="' + url + '">' + url + '</a><br />'
            + '本文章著作权归作者所有，任何形式的转载都请注明出处。';

        selection.selectAllChildren(newdiv);
        window.setTimeout(function () {bodyElement.removeChild(newdiv);}, 200);
    });
</script>


<!-- 代码块功能依赖 -->
<script type="text/javascript" src="/libs/codeBlock/codeBlockFuction.js"></script>

<!-- 代码语言 -->

<script type="text/javascript" src="/libs/codeBlock/codeLang.js"></script>


<!-- 代码块复制 -->

<script type="text/javascript" src="/libs/codeBlock/codeCopy.js"></script>


<!-- 代码块收缩 -->

<script type="text/javascript" src="/libs/codeBlock/codeShrink.js"></script>


<!-- 代码块折行 -->

<style type="text/css">
code[class*="language-"], pre[class*="language-"] { white-space: pre !important; }
</style>


    </div>
    <div id="toc-aside" class="expanded col l3 hide-on-med-and-down">
        <div class="toc-widget card" style="background-color: white;">
            <div class="toc-title"><i class="far fa-list-alt"></i>&nbsp;&nbsp;目录</div>
            <div id="toc-content"></div>
        </div>
    </div>
</div>

<!-- TOC 悬浮按钮. -->

<div id="floating-toc-btn" class="hide-on-med-and-down">
    <a class="btn-floating btn-large bg-color">
        <i class="fas fa-list-ul"></i>
    </a>
</div>


<script src="/libs/tocbot/tocbot.min.js"></script>
<script>
    $(function () {
        tocbot.init({
            tocSelector: '#toc-content',
            contentSelector: '#articleContent',
            headingsOffset: -($(window).height() * 0.4 - 45),
            collapseDepth: Number('0'),
            headingSelector: 'h2, h3, h4'
        });

        // modify the toc link href to support Chinese.
        let i = 0;
        let tocHeading = 'toc-heading-';
        $('#toc-content a').each(function () {
            $(this).attr('href', '#' + tocHeading + (++i));
        });

        // modify the heading title id to support Chinese.
        i = 0;
        $('#articleContent').children('h2, h3, h4').each(function () {
            $(this).attr('id', tocHeading + (++i));
        });

        // Set scroll toc fixed.
        let tocHeight = parseInt($(window).height() * 0.4 - 64);
        let $tocWidget = $('.toc-widget');
        $(window).scroll(function () {
            let scroll = $(window).scrollTop();
            /* add post toc fixed. */
            if (scroll > tocHeight) {
                $tocWidget.addClass('toc-fixed');
            } else {
                $tocWidget.removeClass('toc-fixed');
            }
        });

        
        /* 修复文章卡片 div 的宽度. */
        let fixPostCardWidth = function (srcId, targetId) {
            let srcDiv = $('#' + srcId);
            if (srcDiv.length === 0) {
                return;
            }

            let w = srcDiv.width();
            if (w >= 450) {
                w = w + 21;
            } else if (w >= 350 && w < 450) {
                w = w + 18;
            } else if (w >= 300 && w < 350) {
                w = w + 16;
            } else {
                w = w + 14;
            }
            $('#' + targetId).width(w);
        };

        // 切换TOC目录展开收缩的相关操作.
        const expandedClass = 'expanded';
        let $tocAside = $('#toc-aside');
        let $mainContent = $('#main-content');
        $('#floating-toc-btn .btn-floating').click(function () {
            if ($tocAside.hasClass(expandedClass)) {
                $tocAside.removeClass(expandedClass).hide();
                $mainContent.removeClass('l9');
            } else {
                $tocAside.addClass(expandedClass).show();
                $mainContent.addClass('l9');
            }
            fixPostCardWidth('artDetail', 'prenext-posts');
        });
        
    });
</script>

    

</main>




    <footer class="page-footer bg-color">
    
        <link rel="stylesheet" href="/libs/aplayer/APlayer.min.css">
<style>
    .aplayer .aplayer-lrc p {
        
        display: none;
        
        font-size: 12px;
        font-weight: 700;
        line-height: 16px !important;
    }

    .aplayer .aplayer-lrc p.aplayer-lrc-current {
        
        display: none;
        
        font-size: 15px;
        color: #42b983;
    }

    
    .aplayer.aplayer-fixed.aplayer-narrow .aplayer-body {
        left: -66px !important;
    }

    .aplayer.aplayer-fixed.aplayer-narrow .aplayer-body:hover {
        left: 0px !important;
    }

    
</style>
<div class="">
    
    <div class="row">
        <meting-js class="col l8 offset-l2 m10 offset-m1 s12"
                   server="netease"
                   type="playlist"
                   id="503838841"
                   fixed='true'
                   autoplay='false'
                   theme='#42b983'
                   loop='all'
                   order='random'
                   preload='auto'
                   volume='0.7'
                   list-folded='true'
        >
        </meting-js>
    </div>
</div>

<script src="/libs/aplayer/APlayer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/meting@2/dist/Meting.min.js"></script>

    
    <div class="container row center-align" style="margin-bottom: 0px !important;">
        <div class="col s12 m8 l8 copy-right">
            Copyright&nbsp;&copy;
            
                <span id="year">2020-2021</span>
            
            <span id="year">2020</span>
            <a href="/about" target="_blank">Guo Yaxiang</a>
            |&nbsp;Powered by&nbsp;<a href="https://hexo.io/" target="_blank">Hexo</a>
            |&nbsp;Theme&nbsp;<a href="https://github.com/blinkfox/hexo-theme-matery" target="_blank">Matery</a>
            <br>
            
            
            
            
            
            <br>
            
            <br>
            
        </div>
        <div class="col s12 m4 l4 social-link ">
    <a href="https://github.com/GuoYaxiang" class="tooltipped" target="_blank" data-tooltip="访问我的GitHub" data-position="top" data-delay="50">
        <i class="fab fa-github"></i>
    </a>



    <a href="mailto:YXGyxg112358@163.com" class="tooltipped" target="_blank" data-tooltip="邮件联系我" data-position="top" data-delay="50">
        <i class="fas fa-envelope-open"></i>
    </a>







    <a href="tencent://AddContact/?fromId=50&fromSubId=1&subcmd=all&uin=1431800679" class="tooltipped" target="_blank" data-tooltip="QQ联系我: 1431800679" data-position="top" data-delay="50">
        <i class="fab fa-qq"></i>
    </a>





    <a href="https://www.zhihu.com/people/guo-ya-xiang" class="tooltipped" target="_blank" data-tooltip="关注我的知乎: https://www.zhihu.com/people/guo-ya-xiang" data-position="top" data-delay="50">
        <i class="fab fa-zhihu1">知</i>
    </a>



    <a href="/atom.xml" class="tooltipped" target="_blank" data-tooltip="RSS 订阅" data-position="top" data-delay="50">
        <i class="fas fa-rss"></i>
    </a>

</div>
    </div>
</footer>

<div class="progress-bar"></div>


    <!-- 搜索遮罩框 -->
<div id="searchModal" class="modal">
    <div class="modal-content">
        <div class="search-header">
            <span class="title"><i class="fas fa-search"></i>&nbsp;&nbsp;搜索</span>
            <input type="search" id="searchInput" name="s" placeholder="请输入搜索的关键字"
                   class="search-input">
        </div>
        <div id="searchResult"></div>
    </div>
</div>

<script src="/js/search.js"></script>
<script type="text/javascript">
$(function () {
    searchFunc('/search.xml', 'searchInput', 'searchResult');
});
</script>

    <!-- 回到顶部按钮 -->
<div id="backTop" class="top-scroll">
    <a class="btn-floating btn-large waves-effect waves-light" href="#!">
        <i class="fas fa-arrow-up"></i>
    </a>
</div>


    <script src="/libs/materialize/materialize.min.js"></script>
    <script src="/libs/masonry/masonry.pkgd.min.js"></script>
    <script src="/libs/aos/aos.js"></script>
    <script src="/libs/scrollprogress/scrollProgress.min.js"></script>
    <script src="/libs/lightGallery/js/lightgallery-all.min.js"></script>
    <script src="/js/matery.js"></script>

    <!-- Baidu Analytics -->

    <!-- Baidu Push -->

<script>
    (function () {
        var bp = document.createElement('script');
        var curProtocol = window.location.protocol.split(':')[0];
        if (curProtocol === 'https') {
            bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
        } else {
            bp.src = 'http://push.zhanzhang.baidu.com/push.js';
        }
        var s = document.getElementsByTagName("script")[0];
        s.parentNode.insertBefore(bp, s);
    })();
</script>

    
    <script src="/libs/others/clicklove.js" async="async"></script>
    
    

    
        <script src="//code.tidio.co/zgiqinzrat1rzwxseqhr2pnzsp2a4rjo.js"></script>
        <script>
            $(document).ready(function () {
                setInterval(change_Tidio, 50);
                function change_Tidio() {
                    var tidio=$("#tidio-chat iframe");
                    if(tidio.css("display")=="block"&& $(window).width()>977 ){
                        document.getElementById("tidio-chat-iframe").style.bottom= ($("div#backTop.top-scroll").css("display")=="none" &&$(window).width()>977)>0? "-40px" : ($("div.toc-title").length&&$(window).width()>977)>0?"85px":"20px";
                        document.getElementById("tidio-chat-iframe").style.right="-15px";
                        document.getElementById("tidio-chat-iframe").style.height=parseInt(tidio.css("height"))>=520?"520px":tidio.css("height");
                        document.getElementById("tidio-chat-iframe").style.zIndex="997";
                    }
                    else if(tidio.css("display")=="block"&&$(window).width()>601 &&$(window).width()<992 ){
                        document.getElementById("tidio-chat-iframe").style.bottom= ($("div#backTop.top-scroll").css("display")=="none" && 601< $(window).width()<992)>0? "-40px":"20px" ;
                        document.getElementById("tidio-chat-iframe").style.right="-15px";
                        document.getElementById("tidio-chat-iframe").style.zIndex="997";
                    }
                    else if(tidio.css("display")=="block"&&$(window).width()<601 && parseInt(tidio.css("height"))<230){
                        document.getElementById("tidio-chat-iframe").style.bottom= ($("div#backTop.top-scroll").css("display")=="none" && $(window).width()<601)>0? "-10px":"45px" ;
                        document.getElementById("tidio-chat-iframe").style.zIndex="997";
                    }
                    if( tidio.css("display")=="block"&&$(window).width()<601 && parseInt(tidio.css("height"))>=230){
                        document.getElementById("tidio-chat-iframe").style.zIndex="998";
                    }
                }
            });
        </script>
    

    

    

    

    

    
    <script src="/libs/instantpage/instantpage.js" type="module"></script>
    

</body>

</html>
